/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 d3leaf@126.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mapperdb.builder;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.SelectKey;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.UnknownTypeHandler;

import com.mapperdb.db.ResultEntity;
import com.mapperdb.mapping.EmptySqlSource;

public abstract class BasicBuilder {
	protected ResultEntity resultEntity;
	protected LanguageDriver languageDriver;
	protected SqlSource sqlSource;
	protected SqlCommandType sqlCommandType;
	protected Configuration configuration;
	protected MapperBuilderAssistant assistant;
	protected Class<?>[] parameterTypes;
	protected Class<?> returnType;
	protected Class<?> type;
	protected String name;

	public BasicBuilder(Configuration configuration, Class<?> type) {
		this.type = type;
		this.configuration = configuration;
		String resource = type.getName().replace('.', '/') + ".java (best guess)";
		this.assistant = new MapperBuilderAssistant(configuration, resource);
		this.assistant.setCurrentNamespace(type.getName());
		this.languageDriver = configuration.getDefaultScriptingLanuageInstance();
		this.sqlSource = EmptySqlSource.global;
		this.sqlCommandType = SqlCommandType.SELECT;
	}

	protected String parseResultMap() {
		String resultMapId = generateResultMapName(returnType, parameterTypes);
		applyResultMap(resultMapId, returnType, resultEntity.results());
		return resultMapId;
	}

	protected void applyResultMap(String resultMapId, Class<?> returnType, Result[] results) {
		List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
		// TODO 暂时不做包装类判断
		if (results != null && !returnType.isPrimitive()) {
			for (Result result : results) {
				ArrayList<ResultFlag> flags = new ArrayList<ResultFlag>();
				if (result.id()) flags.add(ResultFlag.ID);
				ResultMapping resultMapping = assistant.buildResultMapping(returnType,//
						nullOrEmpty(result.property()),//
						nullOrEmpty(result.column()), //
						result.javaType() == void.class ? null : result.javaType(),//
						result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),//
						hasNestedSelect(result) ? nestedSelectId(result) : null,//
						null, null, null,//
						result.typeHandler() == UnknownTypeHandler.class ? null : result.typeHandler(), //
						flags);
				resultMappings.add(resultMapping);
			}
		}
		assistant.addResultMap(resultMapId, returnType, null, null, resultMappings, null); // TODO add AutoMappingBehaviour
	}

	/**
	 * @Title: 生成 ResultMap 名称
	 * @param returnType
	 * @param parameterTypes
	 * @return String
	 */
	protected String generateResultMapName(Class<?> returnType, Class<?>... parameterTypes) {
		StringBuilder suffix = new StringBuilder();
		suffix.append("[").append(returnType.getSimpleName()).append("]");

		for (Class<?> c : parameterTypes) {
			suffix.append("-").append(c.getSimpleName());
		}
		if (suffix.length() < 1) {
			suffix.append("-void");
		}
		String name = nullOrEmpty(this.name);
		if (name != null) {
			suffix.insert(0, "." + name);
		}
		return type.getName() + suffix;
	}

	/**
	 * @Title: 获取 参数类型
	 * @param parameterTypes
	 * @return Class<?>
	 */
	protected Class<?> getParameterType(Class<?>... parameterTypes) {
		Class<?> parameterType = null;
		for (int i = 0; i < parameterTypes.length; i++) {
			if (!RowBounds.class.isAssignableFrom(parameterTypes[i])
					&& !ResultHandler.class.isAssignableFrom(parameterTypes[i])) {
				if (parameterType == null) {
					parameterType = parameterTypes[i];
				} else {
					parameterType = Map.class;
				}
			}
		}
		return parameterType;
	}

	protected String nestedSelectId(Result result) {
		String nestedSelect = result.one().select();
		if (nestedSelect.length() < 1) {
			nestedSelect = result.many().select();
		}
		if (!nestedSelect.contains(".")) {
			nestedSelect = type.getName() + "." + nestedSelect;
		}
		return nestedSelect;
	}

	protected boolean hasNestedSelect(Result result) {
		return result.one().select().length() > 0 || result.many().select().length() > 0;
	}

	protected String nullOrEmpty(String value) {
		return value == null || value.trim().length() == 0 ? null : value;
	}

	protected KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId,
			Class<?> parameterTypeClass, LanguageDriver languageDriver) {
		String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
		Class<?> resultTypeClass = selectKeyAnnotation.resultType();
		StatementType statementType = selectKeyAnnotation.statementType();
		String keyProperty = selectKeyAnnotation.keyProperty();
		boolean executeBefore = selectKeyAnnotation.before();

		// defaults
		boolean useCache = false;
		KeyGenerator keyGenerator = new NoKeyGenerator();
		Integer fetchSize = null;
		Integer timeout = null;
		boolean flushCache = false;
		String parameterMap = null;
		String resultMap = null;
		ResultSetType resultSetTypeEnum = null;

		SqlSource sqlSource = buildSqlSourceFromStrings(selectKeyAnnotation.statement(), parameterTypeClass, languageDriver);
		SqlCommandType sqlCommandType = SqlCommandType.SELECT;

		assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
				parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, keyGenerator,
				keyProperty, null, null, languageDriver, null);

		id = assistant.applyCurrentNamespace(id, false);

		MappedStatement keyStatement = configuration.getMappedStatement(id, false);
		SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
		configuration.addKeyGenerator(id, answer);
		return answer;
	}

	protected SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
		final StringBuilder sql = new StringBuilder();
		for (String fragment : strings) {
			sql.append(fragment);
			sql.append(" ");
		}
		return languageDriver.createSqlSource(configuration, sql.toString(), parameterTypeClass);
	}

	// -------------------------------------------------------
	// attr
	// -------------------------------------------------------
	public void setResultEntity(ResultEntity resultEntity) {
		this.resultEntity = resultEntity;
	}

	public void setSqlSource(SqlSource sqlSource) {
		this.sqlSource = sqlSource;
	}

	public void setSqlCommandType(SqlCommandType sqlCommandType) {
		this.sqlCommandType = sqlCommandType;
	}

	public void setParameterTypes(Class<?>[] parameterTypes) {
		this.parameterTypes = parameterTypes;
	}

	public void setReturnType(Class<?> returnType) {
		this.returnType = returnType;
	}

	public void setName(String name) {
		this.name = name;
	}

}
